基于 Bazel的 iOS MonoRepo 实践
0x00 引言
老司机技术周报与字节音乐联合主办的 WWDC.技术沙龙.上海站已于 7月 10 日在上海完美收官。本文将是该场最后一节张忻正(卡比)同学的《基于 Bazel 的 iOS MonoRepo 实践》分享总结。文章内容会继承分享中讲师的绝大部分思想,但是由于篇幅,可能会精简掉一部分内容。此外此文的 PPT 会放在原文链接里面。由于 PR 限制,本文视频部分不对外发布,请大家理解。若对 Bazel 感兴趣可以通过文末方式加 B 站 Bazel 讨论群。
0x01 讲师介绍
讲师:张忻正(卡比)技术专家,14 年加入 B 站,现任 bilibili 移动端架构师,曾维护 ijkplayer 及参与 ffmpeg 贡献,曾基于 bazel 重建 iOS 构建系统。
编辑:红纸,iOS 开发,老司机技术周报编辑,就职于淘系技术部
文章约定,B 站 => BiliBili,讲师 => 张忻正,作者 => 小编。小编 ≠ 讲师
0x02 为什么是 Monorepo
什么是 Monorepo?
在一个「版本控制」仓库包含多个 Projects/Applications/Libraries 。简单通俗点就是 All in one,所有代码就在一个仓库当中。Monorepo 其实并不只是单纯的一种技术,其包含着文化底蕴,团队公司的氛围,B 站在最初设想的时候,一个 Repo 包含前后双端,不过现在还没做到,当下只做到了 iOS 的 Monorepo。
为什么是 Monorepo?
讲师从两个方面阐述了其背景:
迭代模式缩短:随着软件项目的发展,工程的演进方式也发生了改变从传统的瀑布式开发到了敏捷开发,再到现在由 CI/CD 支撑下的班车制流水线式开发,我们可以看到工程迭代的速度正在逐渐缩短,一些同行的竞品也都是达到了周迭代的开发模式。
Muti Repo 带来的高复杂度:当工程的复杂程度越来越高时,工程管理也会开始越来越混乱
Monorepo 带来的特性:
可发现性:代码都在一个仓库下,全部源码,可以随时随地进行搜索,可以最大限度的去参考别人的代码 大规模重构:代码都在一个仓库下面,可以非常简单的进行代码重构 代码复用:最小粒度的去复用别人的代码 没有依赖/没有版本混乱 原子性提交:所有功能的提交都在一个 commit ,像 Muti Repo 可能会存在,代码分布在多个仓库当中,靠依赖解决功能依赖 没有访问限制:大家都可以改动到别人的代码,可能存在别人改坏通用组件的可能性 低性能的构建系统 & Git
0x03 B 站的工程结构化历程
原文阶段共 5 个,小编缩短篇幅,合并了 2 个。
阶段一:单体架构阶段分化,单体内更多的模块
这种模式下会带来什么样的问题呢
多个顶层业务 APP 异化程度越来越显著
超级应用平台
分支管理混乱
Merge hell:合并的时候异常痛苦 阶段二:拆分 Multi Repo
为了解决单体 App 的痛点,B 站进行了多模块的拆分,分治,一个工程解决不了的问题,拆成多个模块,可以减少一定的问题,但是却带来了一些其他新的问题。
都是复制粘贴
引用依赖低于
设计模式乱用
模块间通信及其反人类
用引入问题来解决问题 阶段三:重回 Monorepo
那么古尔丹代价是什么呢?
Human Unreadable:Xcode Project 的管理是一个非常反人类的工程管理方式,相信有过 Monorepo + Xcode 经验的同学都感受过,Merge 时候,如果有人移动文件夹,Merge 的时候将会陷入地狱般的痛苦 Slooooooooow and instability build system:Xcode 编译 + Monorepo 单仓模式,编译一次单架构两三个小时 Merge Hell
0x04 我们真的需要 Xcode 吗?
为什么用 Monorepo & 回答这个问题之前,我们先来看下我们的输入 & 输出是什么?
产物 ipa:资源文件、二进制文件
原料:代码文件(m/cpp/swift/c/h)、签名、资源文件(xcasset/png/json/html/...)、二进制文件(.a/.framework)
那么 Xcode 在这其中扮演的角色就是将原料转换成了编译产物。再将 Xcode 进行拆解,编译的过程其实是调用 Tool Chains 的过程,Xcode 在编译过程中,调用了组合命令 xcodebuild,那么 Xcode 其实就是一个壳。将 xcodebuild 进行拆解,我们就可以得到一系列的编译工具,clang/actool/ibtool/... 等等一系列工具,当我们有了这些命令,我们就可以自己对原料进行加工,输出产物。不再依赖于 Xcode 即可进行编译。
0x05 Why Bazel?
设计原则
Bazel 是 Google 内部 Blaze 的开源版本,Bazel 强调的五个设计原则:
效率:多级 Cache,每一层可以做高效的缓存,本地、远程,来支持增量构建
可扩展性:多种不同的语言可以抽象成一类 Action 进行编译
灵活性:提供内置规则,基于 starlark 的扩展,可以实现各种用户自定义的 rules
正确性:文件缓存是基于文件内容的(而 Xcode 是基于文件时间戳的)
可重复性:有沙盒,不被研发环境影响
B 站在用 Bazel 是在 18 年,使用 0.15.0 的版本,当然现在的 Bazel 对外已经放出了 5.0 的 beta 版本了。
Bazel 所带来的改变
Old 构建集群需要 Clean build,进行重新 build 构建时间非常长 项目需要打开 Xcode,工程大了非常慢 New 构建集群全部是增量编译 集群构建时间缩短到了 10 min 新人入门成本比较高
0x06 Bazel 在 B 站是怎么工作的
说完了 Bazel 所带来的改变,那么支持 Bazel 需要哪些文件呢?其所需要的文件如下两类
- BUILD:描述文件
- WORKSPACE:用于描述项目所需的构建规则
Bazel 的所有描述文件其实都是 Starlark[1] 语法的代码文件,在文件中看到的都是执行函数,大家可以带着这个思想去看着两个文件会更方便理解一些
BUILD:工程描述文件
由于 BUILD 文件规则比较多,这边只是简单介绍,详情参考 Objective-C Rules[2] 和 rule_apple[3]
swift_library: 描述 swift 规则的函数 name:组件名称 srcs:编译源文件 deps:依赖项目 data:资源文件 ios_application:描述如何组织一个 application 的规则 name:Application 名称 app_icons: icon 存放路径 ...
编译 & 研发
在声明完一个工程文件之后,我们就可以使用 Bazel 的一些命令来进行编译了。
bazel build //App:app
其实用 Bazel 编译就这么简单,不像 xcodebuild balabala 后面增加很多很多参数才能进行编译。当然其实这些参数都是被 build 后面的那些编译 rules 给隐藏起来了,如果我们要在 build 后面加参数也是可以的,或者我们可以提供 .bazelrc 将编译参数写在文件里面,让我们的 build 命令看起来更清爽。
OK,回归正题,其实 Bazel build 就是讲上述的 xcodebuild 工具链中的命令进行了封装,在本质上跟 xcodebuild 是没有任何区别的。
那么构建机的爽,研发胖友怎么办?
前面说到 Xcode 编译是个壳,其实 Xcode 还做了其他的很多事情的,比如说语法提示/高亮、LLDB 调试工具等等功能。回到最后,其实暂时还是离不开 Xcode,这里介绍一个工具叫做 tulsi[4],它能做的一件事情呢就是将 Bazel 的配置文件,逆向生成 .xcodeproj 工程。但是这个时候的 .xcodeproj 工程其实已经去掉了 Xcode 的编译功能了。这个时候 Xcode 就是个没有编译功能的壳,但是他还是有上面提到的语法提示/高亮、调试工具等一系列工具集合~
研发安全性
OK,这个时候研发也爽了,但是发展历程上又碰到了问题,代码安全性上的问题。部分核心代码或者敏感信息,即使在非常 Open 的企业文化中,为了数据安全,也不能完全公之于众。由此衍生出下面两种模式。
远端构建机,全源码构建 开发者只能看到有权限的目录代码,没有权限的,将使用编译产物
Fast Build
但是在这种模式下,由于 Bazel 的一个规则,一定的输入等于一定的输出,在这个权限管理的条件下,编译缓存是没办法共享的,每个人都要去 clean build。所以在研发模式(非构建机)情景下,牺牲一定正确性做一个变相提速。
在介绍变相提速前,我们先来介绍一下 c/oc 里面 #include/#import 在预编译过程中都是将整个文件导入到另外一个文件里面去的,顺着这个链路,如果底层的 .h 文件进行修改,整个编译链路都会被破坏掉进行重新编译。
所以 B 站做了一个头文件依赖不传递方案来达到提速效果。
Profilable
相对于编译黑盒的 xcodebuild,Bazel 可以提供输出编译过程的分析内容,我们可以使用 Chrome 自带的 chrome://tracing 来解析编译过程,对每一个 Action 进行分析,查看编译耗时等,为我们提供了良好的可视环境,对后续的优化工作非常有利。
0x07 挑战 & 未来
上手程度:
对于一般的研发同学,只要了解基础的 BUILD 文件格式即可,都不用了解 starlark 的语法, 但是对于想要定制化 rules 的同学,会有一定的难度。 落后的工具链:
比如说 SwiftUI Preview、Metal、Widget,相对于 Apple,Bazel 的工具链支持并没有原生那么便捷。紧急的情况下都是自己去定制 rules 调用官方的工具链,作为探索者,这里就又会有巴拉巴拉一堆小问题存在了。毕竟第三方来支持不会像苹果那样完善。 展望未来:
小编:全量上云毕竟也是一个不小的挑战。换个小一点的 Future。
Xcode 那么难用不要了,换成 VSCode,基于本地的话 LLDB 的问题就很好解决了,写好 DEBUG 的 Plugin。Edit 问题?现在的 Plugin 商店已经有了 Swift Language 的支持了,OC 问题也不大~ 不过 Swift 支持的是 base 在 spm 下的,他背后起了个 lsp server 做代码提示。现在 spm 其实对 UIKit 之类的支持非常不好,这倒的确可是是期待下苹果完善下 spm 的。 开发流程无所谓,输入->处理->输出,那么编译这个处理,我们已经可以利用 Bazel 达到远端集群构建这个目的了,那么 Xcode 这个壳,我们其实也就可以抛弃了,只要我们处理好输入 edit,代码高亮、代码提示,输出(LLDB)等一系列功能,那么云编译也就不是个梦了。 Cloud ide
0x08 最后
多说无用,没有完美的架构解决方案,只有最适合的。如果你想尝试一下 Bazel 带来的新方案,下面这些链接内容,你一定不能错过~当然如果你有任何问题,也欢迎加入到我们的讨论里面来,详情请关注「老司机技术周报」公众号后台私聊 Bazel 加入我们的讨论群。
Bazel: https://bazel.build rules_apple: https://github.com/bazelbuild/rules_apple rules_swift: https://github.com/bazelbuild/rules_swift B站文章:https://bilibili.github.io/2020/07/22/bazel_ios.html LINE 文章:https://engineering.linecorp.com/en/blog/improving-build-performance-line-ios-bazel/
0xFF 最最最重要的东西
上面的东西都不重要,这里才是关键 我大 B 站招人,上面的看不懂?没关系进来手把手教学,包教包会~ 招人链接:https://github.com/bilibili/join-us
参考资料
Starlark: https://github.com/bazelbuild/starlark
[2]Objective-C Rules: https://docs.bazel.build/versions/main/be/objective-c.html
[3]rule_apple: https://github.com/bazelbuild/rules_apple/tree/master/doc
[4]tulsi: https://tulsi.bazel.build/